package com.strangebreeds.therefromhere.engine;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import com.sleepycat.bind.EntryBinding;
import com.sleepycat.bind.serial.SerialBinding;
import com.sleepycat.bind.serial.StoredClassCatalog;
import com.sleepycat.je.Cursor;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.OperationStatus;
import com.strangebreeds.therefromhere.XMLCreator;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.w3c.dom.Element;

import com.strangebreeds.therefromhere.StatusBean;
import com.strangebreeds.therefromhere.PageType;

public class WikiPageEngine
{
	private Logger logger = Logger.getLogger(WikiPageEngine.class);

	private DataAccess dataAccess = new DataAccess();

	private StatusBean statusBean = new StatusBean();

	private long rowNum = 0;

	private String fromPage;
	private String toPage;
	private String strategy;
	private String emailAddress;
	private Integer jobID;

	// The directory the BerkeleyDB files will go
	String workDirectory;

	Database myDatabase = null;
	Database myClassDb = null;
	Database pagesSeenDatabase = null;
	Environment myDbEnvironment = null;

	// For storing the objects
	StoredClassCatalog classCatalog = null;
	EntryBinding dataBinding = null;

	{
		org.apache.log4j.PropertyConfigurator.configure("log4j.properties");
		logger.setLevel(Level.DEBUG);
	}

	public WikiPageEngine(	String fromPage,
							String toPage,
							String strategy,
							String emailAddress,
							Integer jobID)
	{
		this.fromPage = fromPage;
		this.toPage = toPage;
		this.strategy = strategy;
		this.emailAddress = emailAddress;
		this.jobID = jobID;

		// Create the work database
		setupDB();

		// Do the actual work
		runJob();

		// Must be done, so close the work database
		closeDB();

		// And get rid of the files we created
		destroyWorkDirectory(new File(workDirectory));
	}

	private void createWorkDirectory()
	{
		/* Need to put the scratch directory somewhere... */
		workDirectory = Configuration.getBaseWorkDirectory();
		workDirectory += "/";
		workDirectory += fromPage;
		workDirectory += "-";
		workDirectory += toPage;
		workDirectory += "-";
		workDirectory += jobID;

		// Create one directory
		boolean success = (new File(workDirectory)).mkdir();
		if (success)
		{
			logger.debug("Created the " + workDirectory
					+ " directory for job ID " + jobID);
		}
		else
		{
			logger.error("Could not create the directory " + workDirectory
					+ "!");
		}
	}

	private boolean destroyWorkDirectory(File path)
	{
		if (path.exists())
		{
			File[] files = path.listFiles();
			for (int i = 0; i < files.length; i++)
			{
				if (files[i].isDirectory())
				{
					destroyWorkDirectory(files[i]);
				}
				else
				{
					files[i].delete();
				}
			}
		}

		return (path.delete());
	}

	private void setupDB()
	{
		try
		{
			// Create our db environment
			EnvironmentConfig envConfig = new EnvironmentConfig();
			envConfig.setAllowCreate(true);
			/* Disable lock timeouts */
			envConfig.setLockTimeout(0); 
			/* Don't care about locking because everything is done sequentially */
			envConfig.setLocking(false); 
			// envConfig.setTransactional(true); // want transactions (right??)

			createWorkDirectory();

			// Now use the work directory for the work database
			myDbEnvironment = new Environment(new File(workDirectory),
					envConfig);

			// Open the database. Create it if it does not already exist.
			DatabaseConfig dbConfig = new DatabaseConfig();
			dbConfig.setAllowCreate(true);
			myDatabase = myDbEnvironment.openDatabase(null, "wikiDatabase"
					+ jobID.toString(), dbConfig);

			// Open the database that you use to store your class information.
			// The db used to store class information does not require
			// duplicates support.
			dbConfig.setSortedDuplicates(false);
			myClassDb = myDbEnvironment.openDatabase(null, "classDb"
					+ jobID.toString(), dbConfig);

			// Instantiate the class catalog
			classCatalog = new StoredClassCatalog(myClassDb);

			// Create the binding
			dataBinding = new SerialBinding(classCatalog, Page.class);

			// Now do the same for the pages-seen database
			DatabaseConfig pagesSeenConfig = new DatabaseConfig();
			pagesSeenConfig.setAllowCreate(true);
			pagesSeenDatabase = myDbEnvironment
					.openDatabase(null, "wikiPagesSeenDatabase"
							+ jobID.toString(), pagesSeenConfig);
		}
		catch (DatabaseException dbe)
		{
			logger.error("Got DatabaseException: " + dbe.toString(), dbe);
		}
	}

	private void closeDB()
	{
		try
		{
			// Close the pages-seen database
			if (pagesSeenDatabase != null)
			{
				pagesSeenDatabase.close();
			}

			// Close the class database
			if (myClassDb != null)
			{
				myClassDb.close();
			}

			// Close the database
			if (myDatabase != null)
			{
				myDatabase.close();
			}

			// Now close the environment
			if (myDbEnvironment != null)
			{
				myDbEnvironment.cleanLog(); // Clean the log before closing
				myDbEnvironment.close();
			}
		}
		catch (DatabaseException dbe)
		{
			logger.error("Got DatabaseException: " + dbe.toString(), dbe);
		}
	}

	private void runJob()
	{
		Page topPage = new Page(fromPage, true);
		addToWorkDB(topPage);

		// this will hold the last page and its ancestors
		Page finalPage = null;

		boolean isDone = false;

		while (isDone == false)
		{
			Page workPage = getFromWorkDB();

			// Now we send the page name to the topic for a client to receive
			statusBean.sendPageName(jobID, PageType.WORK_PAGE, workPage
					.getPageName());

			if (workPage.getPageName().compareToIgnoreCase(toPage) == 0)
			{
				finalPage = workPage;
				isDone = true;
			}
			else
			{
				List<String> children = dataAccess.getChildPages(workPage
						.getPageName());

				for (String child : children)
				{
					// Now we send the page name to the topic for a client to
					// receive
					statusBean.sendPageName(jobID, PageType.CHILD_PAGE, child);

					if (hasPageBeenSeen(child) == false)
					{
						// First check to see if this is the page we want
						if (child.compareToIgnoreCase(toPage) == 0)
						{
							List<String> ancestors = workPage.getAncestors();
							/*
							 * HACK ALERT! We are checking to see if the last
							 * name in the list is the same that we're trying to
							 * add, and if so, don't add it. This is because
							 * entries were being added multiple times with the
							 * same name
							 */
							if (ancestors.size() > 0)
							{
								if (ancestors.get(ancestors.size() - 1)
										.compareTo(workPage.getPageName()) != 0)
									ancestors.add(workPage.getPageName());
							}

							Page finalChildPage = new Page(child, ancestors);
							finalPage = finalChildPage;

							isDone = true;
							break;
						}
						else
						{
							List<String> ancestors = null;

							if (workPage.isTop())
							{
								ancestors = new ArrayList<String>();
								ancestors.add(workPage.getPageName());

							}
							else
							{
								ancestors = workPage.getAncestors();

								/*
								 * HACK ALERT! We are checking to see if the
								 * last name in the list is the same that we're
								 * trying to add, and if so, don't add it. This
								 * is because entries were being added multiple
								 * times with the same name
								 */
								if (ancestors.size() > 0)
								{
									if (ancestors.get(ancestors.size() - 1)
											.compareTo(workPage.getPageName()) != 0)
										ancestors.add(workPage.getPageName());
								}
							}

							Page childPage = new Page(child, ancestors);
							addToWorkDB(childPage);

							// Now add this to the pages seen db
							addToPageSeenDB(child);
						}
					}
				}
			}
		}

		showLineage(finalPage);
	}

	private void showLineage(Page finalPage)
	{
		logger.debug("--------F I N I S H E D----------");

		XMLCreator xmlResponse = new XMLCreator();
		SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		Element receiptElement = xmlResponse.createXMLElement("receipt");

		/* First let's re-show the original request */
		Element requestElement = xmlResponse.createXMLElement("request");
		// requestElement.setAttribute("submit_time", df.format(new Date()));

		// From Page
		Element fromElement = xmlResponse.createXMLElement("from");
		fromElement.appendChild(xmlResponse.createXMLTextNode(fromPage));
		requestElement.appendChild(fromElement);

		// To Page
		Element toElement = xmlResponse.createXMLElement("to");
		toElement.appendChild(xmlResponse.createXMLTextNode(toPage));
		requestElement.appendChild(toElement);

		// Strategy
		Element strategyElement = xmlResponse.createXMLElement("strategy");
		strategyElement.appendChild(xmlResponse.createXMLTextNode(strategy));
		requestElement.appendChild(strategyElement);

		// Email
		Element emailElement = xmlResponse.createXMLElement("email_address");
		emailElement.appendChild(xmlResponse.createXMLTextNode(emailAddress));
		requestElement.appendChild(emailElement);

		receiptElement.appendChild(requestElement);

		// This is the job ID
		Element jobElement = xmlResponse.createXMLElement("submitted_job");
		jobElement.setAttribute("id", jobID.toString());

		receiptElement.appendChild(jobElement);

		/*
		 * Now let's put our results
		 */
		Element resultsElement = xmlResponse.createXMLElement("results");
		resultsElement.setAttribute("finish_time", df.format(new Date()));
		resultsElement.setAttribute("status",
									"--------F I N I S H E D----------");

		Element ancestorsElement = xmlResponse.createXMLElement("ancestors");

		List<String> ancestors = finalPage.getAncestors();

		for (String ancestor : ancestors)
		{
			Element ancestorElement = xmlResponse.createXMLElement("ancestor");
			ancestorElement
					.appendChild(xmlResponse.createXMLTextNode(ancestor));
			// And add it to the list of ancestors
			ancestorsElement.appendChild(ancestorElement);

			logger.debug("ancestor->" + ancestor);
		}

		// And now our last page
		Element ancestorElement = xmlResponse.createXMLElement("ancestor");
		ancestorElement.appendChild(xmlResponse.createXMLTextNode(finalPage
				.getPageName()));
		// And add it to the list of ancestors
		ancestorsElement.appendChild(ancestorElement);

		logger.debug("final-> " + finalPage.getPageName());

		resultsElement.appendChild(ancestorsElement);
		receiptElement.appendChild(resultsElement);

		xmlResponse.addNode(receiptElement);

		// Now email the person
		EmailResults email = new EmailResults();
		email.setEmailAddress(emailAddress);
		email.setSubject("There From Here Job " + jobID + " finished");
		email.setBody(xmlResponse.getDocAsXMLString(true));

		// Now send it
		if (email.send() == false)
			logger.error("Did not send the email to " + emailAddress);
	}

	private Page getFromWorkDB()
	{
		Page workPage = null;

		try
		{
			Cursor myCursor = myDatabase.openCursor(null, null);

			// Cursors need a pair of DatabaseEntry objects to operate. These
			// hold the key and data found at any given position in the
			// database.
			DatabaseEntry foundKey = new DatabaseEntry();
			DatabaseEntry foundData = new DatabaseEntry();

			if (myCursor.getFirst(foundKey, foundData, LockMode.DEFAULT) == OperationStatus.SUCCESS)
			{
				workPage = (Page) dataBinding.entryToObject(foundData);

				// Now delete it from the database
				myCursor.delete();
			}

			myCursor.close();

		}
		catch (DatabaseException dbe)
		{
			logger.error("Got DatabaseException: " + dbe.toString(), dbe);
		}

		return workPage;
	}

	private void addToWorkDB(Page newPage)
	{
		try
		{
			// Create the DatabaseEntry for the key
			DatabaseEntry theKey = new DatabaseEntry(Long.toString(++rowNum)
					.getBytes("UTF-8"));
			// DatabaseEntry theKey = new DatabaseEntry(newPage.getPageName()
			// .getBytes("UTF-8"));

			// Create the DatabaseEntry for the data. Use the EntryBinding
			// object
			// that was just created to populate the DatabaseEntry
			DatabaseEntry theData = new DatabaseEntry();
			dataBinding.objectToEntry(newPage, theData);

			// Put it as normal
			myDatabase.put(null, theKey, theData);
		}
		catch (DatabaseException dbe)
		{
			logger.error("Got DatabaseException: " + dbe.toString(), dbe);
		}
		catch (UnsupportedEncodingException usee)
		{
			logger.error(	"Got UnsupportedEncodingException: "
									+ usee.toString(),
							usee);
		}
	}

	private void addToPageSeenDB(String name)
	{
		try
		{
			String aKey = name;
			String aData = name;

			DatabaseEntry theKey = new DatabaseEntry(aKey.getBytes("UTF-8"));
			DatabaseEntry theData = new DatabaseEntry(aData.getBytes("UTF-8"));

			// Now we actually insert the record into the database
			pagesSeenDatabase.put(null, theKey, theData);
		}
		catch (DatabaseException dbe)
		{
			logger.error("Got DatabaseException: " + dbe.toString(), dbe);
		}
		catch (UnsupportedEncodingException usee)
		{
			logger.error(	"Got UnsupportedEncodingException: "
									+ usee.toString(),
							usee);
		}
	}

	private boolean hasPageBeenSeen(String pageName)
	{
		boolean wasFound = false;

		try
		{
			// Create a pair of DatabaseEntry objects. theKey
			// is used to perform the search. theData is used
			// to store the data returned by the get() operation.
			DatabaseEntry theKey = new DatabaseEntry(pageName.getBytes("UTF-8"));
			DatabaseEntry theData = new DatabaseEntry();

			// Perform the get.
			if (pagesSeenDatabase.get(null, theKey, theData, LockMode.DEFAULT) == OperationStatus.SUCCESS)
			{
				wasFound = true;
			}
		}
		catch (DatabaseException dbe)
		{
			logger.error("Got DatabaseException: " + dbe.toString(), dbe);
		}
		catch (UnsupportedEncodingException usee)
		{
			logger.error(	"Got UnsupportedEncodingException: "
									+ usee.toString(),
							usee);
		}

		return wasFound;
	}
}
